package birdsong.dummy;

import java.io.IOException;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

import FollowME.ClientToServerMessage;
import FollowME.ServerToClientMessage;
import birdsong.comm.ClientSideMessage;
import birdsong.comm.ClientSideMessage.Type;
import birdsong.comm.ServerComm;
import birdsong.comm.ServerSideMessage;

/**
 * 
 * Communication interface for a Birdsong server. A Birdsong server must
 * communicate clients exclusively through this class.
 * 
 * @author Fernando Pereira, Marcos Martins
 * @version 2.0
 */
public class ServerCommDummy implements ServerComm {

	public static final int SERVER_PORT = 8080;
	
  private ObjectInputMessages oisConnections;
	private boolean gettingConnections = false;
  
  private LinkedBlockingQueue<ClientToServerMessage> receivedMessages;
	private Map<String, ObjectOutputStream> outPutChannels = new HashMap<String, ObjectOutputStream>();
    
   /**
	 * Creates a new ServerCommDummy and the ObjectInputMessages thread
	 */
	public ServerCommDummy() {
		this.receivedMessages = new LinkedBlockingQueue<ClientToServerMessage>();
		this.oisConnections = new ObjectInputMessages();
	}

	/**
	 * Starts the Birdsong server. The method should setup a server socket and
	 * begin to accept connections. This method must return immediately after a
	 * server socket has been setup, and another thread should started to listen
	 * for connections.
	 */
	@Override
	public void start() {
		oisConnections.start();
	}

  /**
	 * Thread used to accept incoming client connections.
	 * 
	 * @author Fernando Pereira, Marcos Martins
	 */
	private class ObjectInputMessages extends Thread {
		private ServerSocket serverSocket;

		@Override
		public void run() {
			try {
				serverSocket = new ServerSocket(SERVER_PORT);

			} catch (IOException e) {
				System.err.println("ERROR - Create server socket.");
			}
			try {
				while (!gettingConnections) {

					Socket socket = serverSocket.accept();

					// thread handler the client with the respective output
					// channel
					new ClientHandler(outPutChannels, socket, receivedMessages).start();
				}
			} catch (IOException e) {
				System.err.println("ERROR - Connection wasn't accept.");
			} finally {
				try {
					System.err.println("Server closed");
					serverSocket.close();
				} catch (IOException e) {
					System.err.println("ERROR - On closing the server socket");
				}
			}
		}
	}

	
	/**
	 * Get the next message received from one of the clients. If no message is
	 * has been received, the method blocks until a message has been received.
	 * 
	 * @return The next message sent by one of the connected clients to the
	 *         server
	 */
	@Override
	public ServerSideMessage getNextMessage() {
		ServerSideMessage newMessage = null;

		try {
			newMessage = receivedMessages.take();
		} catch (InterruptedException e) {
			System.err.println("ERROR - On getting the next message.");
		}

		System.out.println(newMessage.getPayload());

		return newMessage;
	}

	/**
	 * Checks if a message from a client is pending. If
	 * {@link #hasNextMessage()} returns <b>true</b>, a call to
	 * {@link #getNextMessage()} will return immediately with the oldest message
	 * in the queue. If {@link #hasNextMessage()} returns <b>false</b>, a call
	 * to {@link #getNextMessage()} will block until a message has been
	 * received.
	 * 
	 * @return <b>true</b> if a message from a client is currently pending,
	 *         otherwise <b>false</b>.
	 */
	@Override
	public boolean hasNextMessage() {
		return receivedMessages.peek() != null;
	}

	/**
	 * Send a message to a client. This method should be used to relay messages
	 * received from a client to a follower. This method should be called once
	 * for each follower.
	 * 
	 * @param fromNickname
	 *            The nickname of the client from which the message was sent
	 * @param toNickname
	 *            The nickname of a follower
	 * @param birdsongMessage
	 *            The message
	 */
	@Override
	public void sendBirdsongMessage(String fromNickname, String toNickname,
			String birdsongMessage) {
		try {
			// browses toNickname channel in outChannels and writes a new
			// message from fromNicename in it.
			outPutChannels.get(toNickname).writeObject(
					new ServerToClientMessage(Type.BIRDSONG_MESSAGE,birdsongMessage, fromNickname));
							
		} catch (IOException e) {
			System.err.println("ERROR - On sending a birdsong message.");
		}
	}

	/**
	 * Inform a client that a former follower has decided to unfollow.
	 * 
	 * @param toNickname
	 *            nickname of the client who has lost a follower
	 * @param unfollower
	 *            nickname of the unfollower
	 */
	@Override
	public void sendNewFollower(String toNickname, String newFollower) {
		try {
			// searches the outPutChannels from the respective toNickname client
			// and sends the name of the new follower.
			outPutChannels.get(toNickname).writeObject(
					new ServerToClientMessage(Type.FOLLOWER, newFollower,newFollower));
							
		} catch (IOException e) {
			System.err.println("ERROR - New follower message wasn't send.");
		}

	}

	/**
	 * Inform a client that a former follower has decided to unfollow.
	 * 
	 * @param toNickname
	 *            nickname of the client who has lost a follower
	 * @param unfollower
	 *            nickname of the unfollower
	 */
	@Override
	public void sendNewUnfollow(String toNickname, String unfollower) {
		try {
			// searches the outPutChannels from the respective toNickname client
			// and sends the name of the new unfollower.
			outPutChannels.get(toNickname).writeObject(
					new ServerToClientMessage(Type.UNFOLLOWER, unfollower,unfollower));
							
		} catch (IOException e) {
			System.err.println("ERROR - New follower message wasn't send.");
		}
	}

	/**
	 * Report an error to a client. An error can, for instance, be trying to
	 * follow a client who is not connected, trying to unfollow a client not
	 * currently followed, and so on.
	 * 
	 * @param toNickname
	 *            nickname of the client.
	 * @param error
	 *            description of the error.
	 */
	@Override
	public void sendError(String toNickname, String error) {
		try {
			// searches the outPutChannels from the respective toNickname client
			// and sends the desciption of the error
			outPutChannels.get(toNickname).writeObject(
					new ServerToClientMessage(Type.ERROR, error,ServerToClientMessage.SERVER));
							
		} catch (IOException e) {
			System.err.println("ERROR - message wasn't send.");
		}
	}


  /**
	 * Checks if a client with a certain nickname is currently connected.
	 * 
	 * @param nickname
	 *            nickname of the client to check.
	 * @return <b>true</b> if a client with the nickname is currently connected,
	 *         otherwise <b>false</b>.
	 */
	@Override
	public boolean clientIsConnected(String nickname) {
		return outPutChannels.containsKey(nickname);
	}

	/**
	 * Disconnect a client.
	 * 
	 * @param nickname
	 *            nickname of the client to disconnect.
	 */
	@Override
	public void disconnectClient(String nickname) {
		outPutChannels.remove(nickname);
	}

	/**
	 * Inform a client that a new client has connected. When a new client
	 * connects, the server should call this method for each client already
	 * connected to inform the newly connected client about the clients
	 * currently online.
	 * 
	 * @param toNickname
	 *            nickname of the client to whom to send the message.
	 * @param connectedNickname
	 *            nickname of the connected client.
	 */
	@Override
	public void sendClientConnected(String toNickname, String connectedNickname) {
		try {
			outPutChannels.get(toNickname).writeObject(
					new ServerToClientMessage(Type.CONNECTED,connectedNickname, connectedNickname));
		} catch (IOException e) {
			System.err.println("ERROR - message wasn't send.");
		}
	}

	/**
	 * Inform a client that another client disconnected.
	 * 
	 * @param toNickname
	 *            nickname of the client to whom to send the message.
	 * @param disconnectedNickname
	 *            nickname of client who disconnected.
	 */
	@Override
	public void sendClientDisconnected(String toNickname,
			String disconnectedNickname) {

		try {
			outPutChannels.get(toNickname).writeObject(
					new ServerToClientMessage(Type.DISCONNECTED,disconnectedNickname, disconnectedNickname));
							
		} catch (IOException e) {
			System.err.println("ERROR - message wasn't send.");
		}

	}

	/**
	 * Used to return a reference to the thread accepting connections.
	 * 
	 * @return oisConnections thread.
	 */
	public Thread getOisConnectionsThread() {
		return oisConnections;
	}

	/**
	 * Stops the thread from getteing connections.
	 */
	public void stopConnections() {
		gettingConnections = true;
	}
}